Implementing Role-Based Access Control (RBAC) in Spring Security
Course: Advance Java Training
Topic: Implementing Role-Based Access Control (RBAC)
Course Link: Advance Java Training Course
Demo Link: Register for a Free Course Demo
Website: JustAcademy
1. Introduction to Role-Based Access Control
Role-Based Access Control (RBAC) is a security approach in which application permissions are assigned to roles, and roles are assigned to users. Instead of defining individual permissions for every user, an application groups users according to their responsibilities.
In Spring Security, RBAC is commonly implemented using roles such as ADMIN, USER, MANAGER, and EMPLOYEE. These roles determine which URLs, application features, services, and operations an authenticated user can access.
For example, in an e-commerce application, an administrator may manage products and users, a customer may place orders, and a support employee may view customer requests without having permission to delete products.
Why Is RBAC Important?
- Restricts access to sensitive application features.
- Separates user responsibilities and privileges.
- Reduces unauthorized access to business data.
- Makes permission management easier as an application grows.
- Supports the principle of least privilege.
- Helps organizations implement consistent security policies.
2. Learning Objectives
After completing this topic, learners should be able to:
- Understand RBAC and its relationship with authentication and authorization.
- Differentiate between roles, authorities, and permissions.
- Configure role-based URL access using Spring Security.
- Implement method-level security using annotations.
- Assign roles to users and load them through UserDetailsService.
- Understand role prefixes and common authorization errors.
- Test access rules for users with different roles.
- Apply security best practices in Spring Boot applications.
3. Authentication vs. Authorization
Authentication and authorization are related but different security processes. Authentication verifies the identity of a user, while authorization determines which resources that user may access.
| Feature |
Authentication |
Authorization |
| Purpose |
Verifies who the user is. |
Determines what the user can do. |
| Typical question |
Who are you? |
Are you allowed to perform this action? |
| Example |
Validating a username and password. |
Checking whether a user has the ADMIN role. |
| Spring Security concepts |
AuthenticationManager, AuthenticationProvider. |
AuthorizationManager, roles, authorities. |
A user generally needs to authenticate before accessing a protected resource. However, successful authentication alone does not guarantee permission to access every part of the application.
4. Understanding Roles, Authorities, and Permissions
4.1 Roles
A role represents a category of responsibility within an application. Examples include ROLE_ADMIN, ROLE_USER, and ROLE_MANAGER.
4.2 Authorities
An authority is a granted security attribute that Spring Security can evaluate when deciding whether access should be permitted. Authorities can represent roles or more specific privileges.
4.3 Permissions
A permission represents a specific operation, such as PRODUCT_READ, PRODUCT_CREATE, ORDER_UPDATE, or USER_DELETE. Permissions can be useful when an application needs more detailed access rules than broad roles alone provide.
| Concept |
Example |
Purpose |
| Role |
ROLE_ADMIN |
Defines a user's broad responsibility. |
| Role |
ROLE_CUSTOMER |
Identifies a customer account. |
| Authority |
PRODUCT_READ |
Allows a particular operation. |
| Authority |
ORDER_UPDATE |
Allows an order to be updated. |
Spring Security commonly represents roles and permissions as GrantedAuthority values. With the default role prefix, a role named ADMIN is normally represented as ROLE_ADMIN.
5. Example of an RBAC Model
Consider an employee management application with three roles.
| Role |
View Employees |
Add Employees |
Delete Employees |
View Own Profile |
| ADMIN |
Yes |
Yes |
Yes |
Yes |
| MANAGER |
Yes |
Yes |
No |
Yes |
| USER |
No |
No |
No |
Yes |
In this model, the application assigns permissions through roles rather than maintaining a separate list of every operation for every user. The actual authorization rules must still be configured and enforced by the application.
6. Project Setup for Spring Security RBAC
To implement RBAC in Spring Boot, create a project that includes the Spring Web and Spring Security dependencies. Add Spring Data JPA and a database driver if roles and users will be stored in a database.
6.1 Maven Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Use dependency versions managed by your chosen Spring Boot release. For database-backed users, add the appropriate persistence and database dependencies.
7. Implementing RBAC with Spring Security
Spring Security provides request authorization rules that can check whether a user has a particular role or authority. In modern Spring Security configurations, these rules are commonly defined through a SecurityFilterChain bean.
7.1 Create the Security Configuration
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole(
"MANAGER", "ADMIN")
.requestMatchers("/user/**").hasAnyRole(
"USER", "MANAGER", "ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService(
PasswordEncoder passwordEncoder) {
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("changeThisAdminPassword"))
.roles("ADMIN")
.build();
UserDetails manager = User.withUsername("manager")
.password(passwordEncoder.encode("changeThisManagerPassword"))
.roles("MANAGER")
.build();
UserDetails user = User.withUsername("user")
.password(passwordEncoder.encode("changeThisUserPassword"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(
admin, manager, user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Important: This is a learning example using in-memory accounts and HTTP Basic authentication. Replace the sample credentials with secure, managed accounts before deploying an application. Production systems should generally use persistent user records or an appropriate identity provider and HTTPS.
7.2 Understanding the Configuration
@Configuration declares a Spring configuration class.
SecurityFilterChain defines how incoming HTTP requests are secured.
authorizeHttpRequests() configures request-level authorization rules.
requestMatchers() selects URL patterns to protect.
permitAll() allows requests without requiring authentication for that rule.
hasRole("ADMIN") requires the ADMIN role.
hasAnyRole("MANAGER", "ADMIN") permits either specified role.
authenticated() requires an authenticated user but does not, by itself, require a particular role.
InMemoryUserDetailsManager stores sample user details in memory.
BCryptPasswordEncoder hashes passwords for password verification instead of storing plaintext passwords.
8. Understanding the ROLE_ Prefix
By default, Spring Security adds the ROLE_ prefix when evaluating role-based expressions such as hasRole("ADMIN").
For example, the following code creates a user with the ROLE_ADMIN authority:
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("changeThisPassword"))
.roles("ADMIN")
.build();
The equivalent explicit authority can be created with:
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("changeThisPassword"))
.authorities("ROLE_ADMIN")
.build();
Use .roles("ADMIN") when supplying role names without the prefix. Use .authorities("ROLE_ADMIN") when supplying the full authority value. Do not pass ROLE_ADMIN to .roles(), because the default role helper adds the prefix itself.
hasRole() vs. hasAuthority()
| Method |
Example |
Expected authority |
hasRole() |
hasRole("ADMIN") |
ROLE_ADMIN |
hasAuthority() |
hasAuthority("ROLE_ADMIN") |
ROLE_ADMIN |
hasAuthority() |
hasAuthority("PRODUCT_READ") |
PRODUCT_READ |
Use hasRole() for role-based checks and hasAuthority() for exact authority checks. The role prefix can be customized, but applications should use a consistent convention.
9. Creating Controllers for Different Roles
After configuring authorization rules, create controller endpoints that demonstrate the access differences between roles.
package com.example.security;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DashboardController {
@GetMapping("/public/home")
public String publicHome() {
return "Welcome to the public home page.";
}
@GetMapping("/admin/dashboard")
public String adminDashboard() {
return "Welcome to the administrator dashboard.";
}
@GetMapping("/manager/dashboard")
public String managerDashboard() {
return "Welcome to the manager dashboard.";
}
@GetMapping("/user/profile")
public String userProfile() {
return "Welcome to the user profile.";
}
}
When the endpoints are requested, Spring Security checks the authentication and the configured authorization rules before allowing access to the controller.
10. Implementing Method-Level Security
URL-based security protects HTTP endpoints. Method-level security adds authorization checks to individual service methods. It is especially useful when a business operation might be called from multiple controllers or other parts of an application.
10.1 Enable Method Security
package com.example.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
With method security enabled, annotations such as @PreAuthorize can restrict access to service methods.
10.2 Protect a Service Method
package com.example.security;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class EmployeeService {
@PreAuthorize("hasRole('ADMIN')")
public String deleteEmployee(Long employeeId) {
return "Employee " + employeeId + " deleted.";
}
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public String createEmployee(String employeeName) {
return "Employee " + employeeName + " created.";
}
@PreAuthorize("hasRole('USER')")
public String viewPersonalDetails() {
return "Personal details displayed.";
}
}
Here, only an ADMIN can invoke deleteEmployee(), an ADMIN or MANAGER can invoke createEmployee(), and a USER can invoke viewPersonalDetails(). These methods should be called through Spring-managed beans so the security interception can apply.
11. Assigning Roles Dynamically
In real applications, users and roles are usually stored in a database. An administrator may assign a role to a user, or the application may assign roles during account registration after applying its business rules.
A typical relational model contains the following tables:
| Table |
Example Fields |
Purpose |
| users |
id, username, password_hash, enabled |
Stores user account information. |
| roles |
id, name |
Stores available roles such as ROLE_ADMIN. |
| user_roles |
user_id, role_id |
Connects users to their assigned roles. |
A many-to-many relationship is often used because one user may have multiple roles and each role may be assigned to multiple users.
11.1 Example Database Records
users
+----+----------+----------------------+
| id | username | password_hash |
+----+----------+----------------------+
| 1 | anita | encoded-password |
| 2 | rahul | encoded-password |
+----+----------+----------------------+
roles
+----+------------+
| id | name |
+----+------------+
| 1 | ROLE_ADMIN |
| 2 | ROLE_USER |
+----+------------+
user_roles
+---------+---------+
| user_id | role_id |
+---------+---------+
| 1 | 1 |
| 2 | 2 |
+---------+---------+
These sample records illustrate the relationship only. The password values shown are placeholders, not usable password hashes.
11.2 Loading Roles Through UserDetailsService
For database-backed authentication, implement or configure a UserDetailsService that retrieves a user and maps the user's roles to Spring Security authorities.
package com.example.security;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
AppUser appUser = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found"));
List<SimpleGrantedAuthority> authorities =
appUser.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(
role.getName()))
.collect(Collectors.toList());
return User.withUsername(appUser.getUsername())
.password(appUser.getPasswordHash())
.authorities(authorities)
.disabled(!appUser.isEnabled())
.build();
}
}
This example assumes that UserRepository, AppUser, and the role entity have already been implemented. The stored password must be encoded using the same compatible password-encoding strategy used during authentication. The role names should be stored consistently, such as ROLE_ADMIN and ROLE_USER.
12. Restricting Access to Specific Business Operations
RBAC can be extended to distinguish between operations such as reading, creating, updating, and deleting records.
@PreAuthorize("hasAuthority('PRODUCT_READ')")
public Product getProduct(Long id) {
// Retrieve the product.
}
@PreAuthorize("hasAuthority('PRODUCT_CREATE')")
public Product createProduct(Product product) {
// Validate and save the product.
}
@PreAuthorize("hasAuthority('PRODUCT_DELETE')")
public void deleteProduct(Long id) {
// Validate and delete the product.
}
The application must grant these exact authorities to the appropriate users. For example, a product manager may receive PRODUCT_READ and PRODUCT_CREATE but not PRODUCT_DELETE.
This approach combines role-based access with fine-grained permissions. A role can group multiple authorities, allowing administrators to manage access more efficiently.
13. Role-Based Access for REST APIs
In a REST API, Spring Security applies authorization rules to HTTP requests before the request reaches the controller. The response depends on the authentication mechanism and how the application handles access-denied and unauthenticated requests.
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/reports/**")
.hasAnyRole("ADMIN", "MANAGER")
.requestMatchers("/api/profile/**")
.hasAnyRole("USER", "ADMIN", "MANAGER")
.anyRequest().authenticated()
);
return http.build();
}
This is a request-authorization example. A complete API configuration must also select an authentication mechanism appropriate to the application, such as session authentication or OAuth 2.0 resource-server JWT validation.
For stateless token-based APIs, configure token validation and session behavior deliberately. Do not disable CSRF protection automatically without first evaluating whether the API uses browser-managed credentials such as cookies.
14. Understanding the RBAC Request Flow
- A user sends a request to a protected endpoint.
- Spring Security's filter chain processes the request.
- The configured authentication mechanism establishes the user's identity.
- The authenticated principal contains the granted authorities.
- The authorization rule checks the requested URL or protected method.
- If the user has the required role or authority, the operation is allowed.
- If access is denied, Spring Security applies the configured denial handling.
For example, if a user with ROLE_USER requests /admin/dashboard, the rule hasRole("ADMIN") fails. The application denies access even though the user has successfully authenticated.
15. Handling Access-Denied Errors
Spring Security distinguishes between a user who has not authenticated and a user who is authenticated but lacks the required permission.
- Unauthenticated request: commonly results in an authentication entry point being invoked, such as a login redirect or HTTP 401 response, depending on configuration.
- Authenticated but unauthorized request: commonly results in an access-denied response, often HTTP 403.
For REST APIs, configure appropriate JSON error responses if the application needs a consistent API response format. Avoid returning internal exception details or sensitive information to clients.
16. Testing Role-Based Access Control
RBAC must be tested with users who have different roles. A successful login test alone is not enough to prove that authorization rules are correct.
16.1 Access Test Matrix
| Request |
USER |
MANAGER |
ADMIN |
/public/home |
Allowed |
Allowed |
Allowed |
/admin/dashboard |
Denied |
Denied |
Allowed |
/manager/dashboard |
Denied |
Allowed |
Allowed |
/user/profile |
Allowed |
Allowed |
Allowed |
16.2 Example Testing with Mock Users
When using Spring Security's test support, role-based tests can be written with @WithMockUser. Add the compatible Spring Security test dependency to the project.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.security.test.web.servlet.request
.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request
.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.result
.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request
.MockMvcRequestBuilders.get;
@SpringBootTest
@AutoConfigureMockMvc
class RoleAccessTest {
@Autowired
private MockMvc mockMvc;
@Test
void adminCanAccessAdminDashboard() throws Exception {
mockMvc.perform(get("/admin/dashboard")
.with(user("admin").roles("ADMIN")))
.andExpect(status().isOk());
}
@Test
void normalUserCannotAccessAdminDashboard()
throws Exception {
mockMvc.perform(get("/admin/dashboard")
.with(user("user").roles("USER")))
.andExpect(status().isForbidden());
}
@Test
void anonymousUserCannotAccessAdminDashboard()
throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isUnauthorized());
}
}
The final test expects HTTP 401, which is appropriate only when the application is configured with an entry point that returns 401 for unauthenticated requests. With form login or another entry point, the expected result may instead be a redirect. Adapt tests to the actual security configuration. For methods that require CSRF protection, include a valid CSRF token in state-changing requests.
17. Common RBAC Errors and Troubleshooting
| Problem |
Possible Cause |
Solution |
| ADMIN cannot access an admin endpoint. |
Role authority is missing or has an incorrect name. |
Verify that the granted authority is ROLE_ADMIN when using the default prefix. |
| All users receive access denied. |
Authorization rules are too restrictive or roles are not loaded. |
Review matcher order and inspect the authenticated user's authorities. |
| hasRole() does not match. |
The role name was configured inconsistently. |
Use hasRole("ADMIN") with ROLE_ADMIN, or align the configured prefix. |
| A protected URL is publicly accessible. |
A broad public matcher or incorrect rule order may apply. |
Review all matchers and place specific rules before broad rules. |
| Method security does not run. |
Method security is not enabled or invocation bypasses the Spring proxy. |
Enable @EnableMethodSecurity and invoke protected methods through the managed bean. |
| Users have outdated permissions. |
Roles may be cached in an existing session or token. |
Review session, cache, and token lifetimes and revoke or refresh credentials as appropriate. |
| Tests return redirects instead of 401. |
The configured authentication entry point redirects to a login page. |
Adjust the test expectation or configure an API-specific entry point. |
18. Security Best Practices for RBAC
- Follow least privilege: grant users only the roles and permissions required for their work.
- Protect every sensitive operation: use URL and method-level authorization where appropriate.
- Use secure password storage: store password hashes, not plaintext passwords.
- Use HTTPS: protect credentials and session data while in transit.
- Validate role assignments: only authorized administrators or trusted workflows should grant privileged roles.
- Use deny-by-default rules: explicitly define which endpoints are public and protect the rest.
- Test negative scenarios: confirm that unauthorized users cannot access protected endpoints or methods.
- Avoid hard-coded production credentials: use secure configuration and a suitable identity-management system.
- Audit privileged actions: record important administrative operations without logging passwords or secrets.
- Review role changes: ensure that removed permissions take effect according to session and token policies.
- Do not rely on frontend hiding: hiding a button is not a substitute for server-side authorization.
- Use appropriate CSRF protection: especially when browser sessions or cookies authenticate requests.
19. Practical Project: Employee Management System
Build a Spring Boot employee management system that uses RBAC to protect employee records and administrative operations.
Project Requirements
- Create user accounts with securely stored password hashes.
- Define ADMIN, MANAGER, and USER roles.
- Allow administrators to create, update, and delete employee records.
- Allow managers to view employees and create permitted records.
- Allow ordinary users to view their own profile.
- Protect endpoints using
SecurityFilterChain.
- Apply method-level restrictions to important service methods.
- Return suitable authentication and access-denied responses.
- Write automated tests for permitted and forbidden requests.
Suggested Endpoint Design
| Endpoint |
Allowed Roles |
Purpose |
GET /employees |
ADMIN, MANAGER |
View employee records. |
POST /employees |
ADMIN, MANAGER |
Create an employee. |
PUT /employees/{id} |
ADMIN, MANAGER |
Update an employee subject to business rules. |
DELETE /employees/{id} |
ADMIN |
Delete an employee. |
GET /profile |
Authenticated user |
View the current user's profile. |
For the profile endpoint, also ensure the application retrieves data belonging to the authenticated user. Authorization to access the endpoint does not automatically guarantee that the user can access only their own records.
20. Interview Questions and Answers
Q1. What is Role-Based Access Control?
RBAC is an authorization model that assigns permissions to roles and roles to users. Access is determined by the roles and permissions granted to the authenticated user.
Q2. What is the difference between authentication and authorization?
Authentication verifies a user's identity. Authorization determines whether the user has permission to perform an operation or access a resource.
Q3. What is the difference between a role and an authority in Spring Security?
A role is a broad category of access, such as ADMIN. An authority is a granted security value that can represent a role or a specific permission. Spring Security commonly uses the ROLE_ prefix for roles.
Q4. What is the difference between hasRole() and hasAuthority()?
hasRole("ADMIN") checks for the default role authority ROLE_ADMIN. hasAuthority("PRODUCT_READ") checks for the exact authority string PRODUCT_READ.
Q5. What is SecurityFilterChain?
SecurityFilterChain defines the security filters and request-handling security configuration applied to matching HTTP requests.
Q6. How can you restrict an endpoint to administrators?
Configure a rule such as requestMatchers("/admin/**").hasRole("ADMIN") or protect a service method with @PreAuthorize("hasRole('ADMIN')").
Q7. What is @EnableMethodSecurity?
It enables Spring Security method-level authorization features, including annotations such as @PreAuthorize.
Q8. Why should authorization be checked on the backend?
Frontend restrictions can be bypassed. Backend authorization ensures that protected resources and operations are checked regardless of how the request is sent.
Q9. What is the principle of least privilege?
It means granting each user only the permissions necessary to perform their assigned responsibilities.
Q10. How do you implement database-backed roles?
Store users and roles in database tables, associate users with roles, and load the corresponding authorities through a configured user-details or authentication service.
Q11. Why might an authorized user receive HTTP 403?
The user's authorities may not match the configured rule, the role prefix may be inconsistent, or another security policy may deny the request.
Q12. Is RBAC alone sufficient for every application?
No. Some systems also require resource ownership checks, tenant isolation, attribute-based rules, or object-level permissions. RBAC should be combined with these controls when business requirements demand them.
21. Quick Revision Checklist
- RBAC maps roles to users and permissions to roles.
- Authentication verifies identity; authorization controls access.
hasRole("ADMIN") normally checks for ROLE_ADMIN.
hasAuthority() checks an exact authority value.
SecurityFilterChain configures HTTP request security.
@EnableMethodSecurity enables method-level security.
@PreAuthorize protects individual methods.
- Database-backed applications can load roles through
UserDetailsService.
- Test both allowed and denied access for every important role.
- Use least privilege, secure password storage, HTTPS, and appropriate error handling.
22. Conclusion
Role-Based Access Control is an important part of implementing secure Java applications with Spring Security. It enables developers to organize users by responsibility and enforce consistent authorization rules across URLs, REST APIs, and business service methods.
A reliable RBAC implementation combines well-defined roles, carefully assigned authorities, secure authentication, server-side authorization, and automated testing. For more complex applications, role checks can be supplemented with resource ownership and fine-grained permission checks.
To continue learning, visit the Advance Java Training course, register for a course demo, or explore the JustAcademy website.
23. Additional Learning Resources